/*
 * Copyright (c) 2010-2011, Yawning <yawninglol at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package yawning.mew.cat;

import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import yawning.mew.BuffsDebuffs.Debuff;
import yawning.mew.ProcEffect;
import yawning.mew.ProcEffect.Effect;
import yawning.mew.Procs;
import yawning.mew.cat.CatModelSimulation.Action;
import yawning.mew.cat.CatModelSimulation.SimulatorProperty;
import yawning.mew.character.CharacterModel.AttackResult;
import yawning.mew.sim.SimulationAction;
import yawning.mew.sim.SimulationEngine;
import yawning.mew.sim.SimulationEngine.AfterActionsCallback;
import yawning.mew.sim.SimulationLog;
import yawning.mew.sim.SimulationProc;
import yawning.mew.sim.SimulationManager.Property;
import yawning.mew.sim.SimulationProc.EffectCallback;
import yawning.mew.sim.SimulationProcStatus;
import yawning.mew.sim.SimulationTarget;
import yawning.mew.sim.Strategy.IStatus;
import yawning.mew.target.Target;

public class CatModelSimState implements IStatus {
	// Begin Public Methods available to Strategy
	public final double getEncounterDuration() {
		return mSimEngine.getEndTime();
	}

	public final double getTimeRemaining() {
		return mSimEngine.getEndTime() - mSimEngine.getSimTime();
	}

	public final double getTimeToDie() {
		if (mActiveTargetData == null) return 0.0;

		return mActiveTargetData.mDeathEv.getExpiryTime() - mSimEngine.getSimTime();
	}

	public final double getElapsedTime() {
		return mSimEngine.getSimTime();
	}

	public final int getLevel() {
		return mSimEngine.getToon().level;
	}

	public final boolean isInBloodInTheWaterRange() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mBloodInTheWaterEv.mInRange;
	}

	public final boolean isInCombat() {
		return mSimEngine.getSimTime() > 0.0;
	}

	public final boolean isInGCD() {
		return mSimEngine.isInGCD();
	}

	public final boolean isAutoAttackEnabled() {
		return mMeleeSwingEv.isUp();
	}

	public final boolean isFrontalAttackOnly() {
		if (mActiveTargetData == null) return mSimEngine.getConfig().getBool(SimulatorProperty.FRONTALATTACKS);	// Well, just report the default I guess.
		return mActiveTargetData.mFrontalAttacksOnly;
	}

	public final double getMissChance() {
		return mModel.getMissedChance();
	}

	public final double getDodgeChance() {
		return mModel.getDodgedChance();
	}

	public final double getCritChance() {
		return mModel.getMeleeCritChance();
	}

	public final double getBaseCritChance() {
		return mBaseCritChance;
	}

	public final int getEnergy() {
		return mEnergyTickEv.mEnergy;
	}

	public final int getEnergyCap() {
		return mEnergyTickEv.mEnergyCap;
	}

	public final double getEnergyTickInterval() {
		return mModel.getEnergyTickInterval();
	}

	public final int getComboPoints() {
		if (mActiveTarget != mCPTarget) return 0;

		return mComboPoints;
	}

	public final boolean isFeralFFUp() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mFeralFFAction.mStacks > 0;
	}

	public final int getFeralFFStacks() {
		if (mActiveTargetData == null) return 0;
		return mActiveTargetData.mFeralFFAction.mStacks;
	}

	public final double getFeralFFRemaining() {
		return isFeralFFUp() ? mActiveTargetData.mFeralFFAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getFeralFFCD() {
		return mFeralFFCooldownExpireTime > mSimEngine.getSimTime() ? mFeralFFCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isOoCUp() {
		return mOocEv.isUp();
	}

	public final int getPounceCost() {
		return getEnergyCost(mModel.pounceCost);
	}

	public final int getRavageCost() {
		if (mStampedeEv.isUp()) return mModel.ravageCostStampede; else return getEnergyCost(mModel.ravageCost);
	}

	public final int getClawCost() {
		return getEnergyCost(mModel.clawCost);
	}

	public final boolean isMangleUp() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mMangleAction.isUp();
	}

	public final double getMangleRemaining() {
		return isMangleUp() ? mActiveTargetData.mMangleAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final int getMangleCost() {
		return getEnergyCost(mModel.mangleCost);
	}

	public final int getShredCost() {
		return getEnergyCost(mModel.shredCost);
	}

	public final int getRakeCost() {
		return getEnergyCost(mModel.rakeCost);
	}

	public final int getRipCost() {
		return getEnergyCost(mModel.ripCost);
	}

	public final int getMaimCost() {
		return getEnergyCost(mModel.maimCost);
	}

	public final int getSwipeCost() {
		return getEnergyCost(mModel.swipeCost);
	}

	public final boolean isRakeUp() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mRakeAction.isUp();
	}

	public final double getRakeRemaining() {
		return isRakeUp() ? mActiveTargetData.mRakeAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getRakeBaseDuration() {
		return mModel.getRakeDebuffDuration();
	}

	public final boolean isRakeTFed() {
		return isRakeUp() && mActiveTargetData.mRakeAction.mTigersFury;
	}

	public final boolean isRipUp() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mRipAction.isUp();
	}

	public final double getRipRemaining() {
		return isRipUp() ? mActiveTargetData.mRipAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getRipBaseDuration() {
		return mModel.getRipDebuffDuration();
	}

	public final int getRipGlyphExtRemaining() {
		if (!isRipUp()) return 0;
		return mModel.ripGlyphExtDuration > 0 ? mModel.ripGlyphExtMaxTicks - mActiveTargetData.mRipAction.mGlyphExts : 0;
	}

	public final boolean isRipTFed() {
		return isRipUp() && mActiveTargetData.mRipAction.mTigersFury;
	}

	public final boolean isSavageRoarUp() {
		return mSavageRoarEv.isUp();
	}

	public final double getSavageRoarRemaining() {
		return mSavageRoarEv.isUp() ? mSavageRoarEv.getExpiryTime() - mSimEngine.getSimTime() :  0.0;
	}

	public final int getSavageRoarCost() {
		return getEnergyCost(mModel.srCost);
	}

	public final int getFerociousBiteCost() {
		return getEnergyCost(mModel.biteCost);
	}

	public final boolean isBerserkUp() {
		return mBerserkEv.isUp();
	}

	public final double getBerserkCD() {
		return mBerserkEv.mCooldownExpireTime > mSimEngine.getSimTime() ? mBerserkEv.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getBerserkRemaining() {
		return mBerserkEv.isUp() ? mBerserkEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getBerserkBaseDuration() {
		return mModel.berserkBuffDuration;
	}

	public final boolean isTigersFuryUp() {
		return mTigersFuryEv.isUp();
	}

	public final double getTigersFuryCD() {
		return mTigersFuryEv.mCooldownAt > mSimEngine.getSimTime() ? mTigersFuryEv.mCooldownAt - mSimEngine.getSimTime() : 0.0;
	}

	public final double getTigersFuryRemaining() {
		return mTigersFuryEv.isUp() ? mTigersFuryEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getFeralChargeCD() {
		return mFeralChargeAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mFeralChargeAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getMaimCD() {
		return mMaimCooldownExpireTime > mSimEngine.getSimTime() ? mMaimCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isStampedeUp() {
		return mStampedeEv.isUp();
	}

	public final double getStampedeRemaining() {
		return mStampedeEv.isUp() ? mStampedeEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getSkullBashCD() {
		return mSkullBashCooldownExpireTime > mSimEngine.getSimTime() ? mSkullBashCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final int getSkullBashCost() {
		return getEnergyCost(mModel.skullBashCost);
	}

	public final boolean isHeroismUp() {
		return mHeroismEv.isUp();
	}

	public final double getHeroismCD() {
		return mHeroismEv.mCooldownExpireTime > mSimEngine.getSimTime() ? mHeroismEv.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getHeroismRemaining() {
		return mHeroismEv.isUp() ? mHeroismEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isUnholyFrenzyUp() {
		return mUnholyFrenzyEv.isUp();
	}

	public final double getUnholyFrenzyCD() {
		return mUnholyFrenzyEv.mCooldownExpireTime > mSimEngine.getSimTime() ? mUnholyFrenzyEv.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getUnholyFrenzyRemaining() {
		return mUnholyFrenzyEv.isUp() ? mUnholyFrenzyEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isShatteringThrowUp() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mShatteringThrowEv.isUp();
	}

	public final double getShatteringThrowCD() {
		return mShatteringThrowCooldownExpireTime > mSimEngine.getSimTime() ? mShatteringThrowCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getShatteringThrowRemaining() {
		return isShatteringThrowUp() ? mActiveTargetData.mShatteringThrowEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isToTTUp() {
		// The event is always up if enabled since I use it to schedule it being called periodically,
		// so just check for the multiplier instead.
		return mTottEv.getMult() != 1.0;
	}

	public final double getToTTRemaining() {
		return isToTTUp() ? mTottEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean hasPotion() {
		return mHavePotion;
	}

	public final boolean canPotion() {
		// Don't potion out of combat if the user does not want pre-potion.
		if (!mPrePotion && !isInCombat()) return false;

		return mPotionAction.canPotion();
	}

	public final boolean hasTier_11_4pc() {
		return mModel.druid.tier_11_4pc;
	}

	public final boolean isTier_11_4pcUp() {
		return mTier_11_4pcEv.isUp();
	}

	public final int getTier_11_4pcStacks() {
		return (int) mModel.tier_11_4pc_buffStacks;
	}

	public final double getTier_11_4pcRemaining() {
		return mModel.tier_11_4pc_buffStacks > 0 ? mTier_11_4pcEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean hasTier_12_4pc() {
		return mModel.druid.tier_12_4pc;
	}

	public final boolean hasTier_13_4pc() {
		return mModel.druid.tier_13_4pc;
	}

	public final boolean shouldFF() {
		return mShouldFF;
	}

	public final boolean shouldMangle() {
		return mShouldMangle;
	}

	public final boolean isInMeleeRange() {
		return mIsInMeleeRange;
	}

	public final boolean shouldAggressiveFeralCharge() {
		// Should the script attempt to use Stampede + Ravage! beyond the opener?
		return mAggressiveFeralCharge;
	}

	public final boolean canFeralCharge() {
		return (!mBurningTendon) && (getFeralChargeCD() == 0.0) && (!isInMeleeRange() || getFeralChargeTotalTime() == 0.0);
	}

	public final double getFeralChargeTotalTime() {
		// How long will it take to gain distance + Feral Charge?
		return mRunAwayEv.mRunTime;
	}

	public final boolean isStunned() {
		if (mActiveTarget == null) return false;

		return mActiveTarget.getTargetStunEv().isUp();
	}

	public final double getStunRemaining() {
		if (mActiveTarget == null) return 0.0;

		return mActiveTarget.getTargetStunEv().isUp() ? mActiveTarget.getTargetStunEv().getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean shouldSkullBash() {
		if (mActiveTarget == null) return false;
		return mActiveTarget.getTargetCastEv().isUp();
	}

	public final boolean isTargetCasting() {
		if (mActiveTarget == null) return false;
		return mActiveTarget.getTargetCastEv().isCasting();
	}

	public final double nextCastIn() {
		if (mActiveTarget == null) return 0.0;
		return mActiveTarget.getTargetCastEv().nextCastIn();
	}

	public final double getTargetCastTime() {
		if (mActiveTarget == null) return 0.0;
		return mActiveTarget.getTargetCastEv().getTargetCastTime();
	}

	public final double nextUtilityCastIn() {
		boolean willRebirth = mRebirthEv.isUp() && (mRebirthEv.isCasting() == false);
		boolean willTranquility = mTranquilityEv.isUp() && (mTranquilityEv.isCasting() == false);

		double rval = Double.MAX_VALUE;
		if (willTranquility || willRebirth) {
			double rebirthIn = (willRebirth) ? mRebirthEv.getStartTime() - mSimEngine.getSimTime() : Double.MAX_VALUE;
			double tranquilityIn = (willTranquility) ? mTranquilityEv.getStartTime() - mSimEngine.getSimTime() : Double.MAX_VALUE;

			rval = Math.min(rebirthIn, tranquilityIn);
		}

		return rval;
	}

	public final Map<String, SimulationProcStatus> getProcStatusMap() {
		return mSimEngine.getProcStatusMap();
	}

	public final double getTargetTimeToDie() {
		if (mActiveTarget == null) return 0.0;
		return Math.max(mActiveTargetData.mDeathEv.getExpiryTime() - mSimEngine.getSimTime(), 0);
	}

	public final String getTarget() {
		if (mActiveTarget == null) return null;
		return mActiveTarget.getTarget().getName();
	}

	public final int getTargetLevel() {
		if (mActiveTarget == null)
			throw new RuntimeException("Attempted to query level of non-existing target.");
		return mActiveTarget.getTarget().getLevel();
	}

	public final String getComboPointsTarget() {
		if (getComboPoints() == 0 || mCPTarget == null) return null;
		return mCPTarget.getTarget().getName();
	}

	public final int getTargetCount() {
		return mTargetList.size();
	}

	public final List<String> getTargetList() {
		return mTargetList;
	}

	public final List<String> getIdleTargetList() {
		return mIdleTargetList;
	}

	public final boolean setTarget(String name) {
		if (name == null) {
			// Ok, you want to detarget.
			swapToTarget(null, false);

			return true;
		}

		SimulationTarget target = mTargetMap.get(name);
		if (target != null) {
			swapToTarget(target, false);

			return mActiveTarget == target;
		}

		return false;
	}

	public final boolean setTargetAfterAction(String name) {
		if (!mTargetList.contains(name)) return false;
		if (mTargetAfterAction != null)
			throw new RuntimeException("Already have a target queued for switch back.");

		mTargetAfterAction = name;
		mSimEngine.addAfterActionCallback(new AfterActionsCallback() {
			@Override
			public void onAfterActions() {
				setTarget(mTargetAfterAction);
				mTargetAfterAction = null;
			}
		});

		return true;
	}

	public final void newTarget(String name, int targetLevel, double targetTimeToDie, boolean honorBitW) {
		if (name.length() == 0)
			throw new RuntimeException("Must provide a valid target name.");
		if (targetTimeToDie <= 0)
			throw new RuntimeException("Must provide a valid time to die.");

		SimulationTarget simTarget = mTargetMap.get(name);
		if (simTarget != null)
			throw new RuntimeException("Target names must be unique, \"" + name + "\" is not.");

		Target target = new Target(mModel.mToon, name, targetLevel, targetTimeToDie);
		simTarget = new SimulationTarget(target, mSimEngine);
		simTarget.setTargetData(new CatTargetData(simTarget, honorBitW));
		mTargetMap.put(name, simTarget);
		mTargetList.add(name);
		mTargetSpawnOrderList.add(name);

		if (mIsLogging) mLog.log("Target: %s Spawned\n", name);
	}

	public final void newTarget(String name, int targetLevel, double targetTimeToDie, double bitwTime) {
		newTarget(name, targetLevel, targetTimeToDie, false);

		SimulationTarget simTarget = mTargetMap.get(name);
		((CatTargetData) simTarget.getTargetData()).mBloodInTheWaterEv.scheduleStartTime(bitwTime);
	}

	public final void setTargetFacing(String name, boolean isFrontal) {
		SimulationTarget target = mTargetMap.get(name);
		if (target == null)
			throw new RuntimeException("Attempted to set facing on a non-existent target, \"" + name + "\"");

		CatTargetData targetData = (CatTargetData) target.getTargetData();
		targetData.mFrontalAttacksOnly = isFrontal;
		targetData.mCanParry = isFrontal & !target.getTarget().getCannotParry();
		if (isFrontal) targetData.mShouldReportParry = true;	// If we every could have been parried report it.

		if (mIsLogging) mLog.log("Target: %s Facing: %s\n", name, (isFrontal) ? "Front" : "Back");
	}

	public final void setTargetActive(String name, boolean isActive) {
		SimulationTarget target = mTargetMap.get(name);
		if (target == null)
			throw new RuntimeException("Attempted to set active status on a non-existent target, \"" + name + "\"");

		// Do stuff.
		if (isActive) {
			if (mTargetList.contains(name)) {
				return;
			}
			mIdleTargetList.remove(name);
			mTargetList.add(name);
			target.getTargetCastEv().restart();
		} else {
			if (mIdleTargetList.contains(name)) {
				return;
			}
			mTargetList.remove(name);
			mIdleTargetList.add(name);

			if (mActiveTarget == target) {
				// We just deactivated our current target, so deselect our target.
				setTarget(null);
			}
			target.getTargetCastEv().stop();
		}

		if (mIsLogging) mLog.log("Target: %s Active: %s\n", name, isActive);
	}

	public final void scheduleUtilityTimer(double when) {
		mSimEngine.getUtilityTimer().scheduleCallback(when);
	}

	public final void scheduleIdleTime(double length) {
		mIdleTimeExpireTime = mSimEngine.getSimTime() + length;
	}

	public final Action getLastTriggeredAction() {
		return (Action) mSimEngine.getLastTriggeredAction();
	}
	// End Public Methods

	class CatTargetData extends SimulationTarget.TargetData {
		CatTargetData(SimulationTarget t, boolean allowBitW) {
			t.super(mSimEngine);
			mSpawnTime = mSimEngine.getSimTime();
			mFrontalAttacksOnly = mSimEngine.getConfig().getBool(SimulatorProperty.FRONTALATTACKS);
			mCanParry = mFrontalAttacksOnly & !getSimulationTarget().getTarget().getCannotParry();
			mShouldReportParry = this.mCanParry;

			mShatteringThrowEv = new EventShatteringThrow(CatModelSimState.this, this);
			mBloodInTheWaterEv = new EventBloodInTheWaterRange(CatModelSimState.this, this, allowBitW);
			mTier_12_2pcEv = new EventTier_12_2pc(CatModelSimState.this, this);

			mFeralFFAction = new EventFeralFF(CatModelSimState.this, this);
			mMangleAction = new EventMangleDebuff(CatModelSimState.this, this);
			mRakeAction = new EventRake(CatModelSimState.this, this);
			mRipAction = new EventRip(CatModelSimState.this, this);
			mPounceAction = new EventPounce(CatModelSimState.this, this);

			mClawAction = new ClawAction(CatModelSimState.this, this);
			mShredAction = new ShredAction(CatModelSimState.this, this);
			mRavageAction = new RavageAction(CatModelSimState.this, this);
			mFerociousBiteAction = new FerociousBiteAction(CatModelSimState.this, this);
			mMaimAction = new MaimAction(CatModelSimState.this, this);
			mSkullBashAction = new SkullBashAction(CatModelSimState.this, this);
		}

		@Override
		public void onTargetDeath() {
			mShatteringThrowEv.cancel();
			mBloodInTheWaterEv.cancel();
			mTier_12_2pcEv.cancel();
			mFeralFFAction.cancel();
			mMangleAction.cancel();
			mRakeAction.cancel();
			mRipAction.cancel();
			mPounceAction.cancel();

			mTargetList.remove(getSimulationTarget().getTarget().getName());
			mIdleTargetList.remove(getSimulationTarget().getTarget().getName());

			// Eeep, our current target died, kill the swing timer and detarget stuff.
			if (mActiveTarget == getSimulationTarget()) {
				mMeleeSwingEv.stop();
				swapToTarget(null, false);
			}

			if (mIsLogging) mLog.log("Target: %s Death\n", getSimulationTarget().getTarget().getName());
		}

		EventShatteringThrow mShatteringThrowEv;
		EventBloodInTheWaterRange mBloodInTheWaterEv;
		EventTier_12_2pc mTier_12_2pcEv;

		EventFeralFF mFeralFFAction;
		EventMangleDebuff mMangleAction;
		EventRake mRakeAction;
		EventRip mRipAction;
		EventPounce mPounceAction;
		ClawAction mClawAction;
		ShredAction mShredAction;
		RavageAction mRavageAction;
		FerociousBiteAction mFerociousBiteAction;
		MaimAction mMaimAction;
		SkullBashAction mSkullBashAction;

		long mProcSpellDamage;
		long mProcPhysDamage;

		double mSpawnTime;
		boolean mCanParry;
		boolean mShouldReportParry;
		boolean mFrontalAttacksOnly;
	}

	SimulationEngine mSimEngine;
	CatModel mModel;
	boolean mIsLogging = false;
	SimulationLog mLog;

	SimulationTarget mActiveTarget;
	CatTargetData mActiveTargetData;
	Map<String,SimulationTarget> mTargetMap;
	List<String> mTargetSpawnOrderList;
	private List<String> mTargetList;
	private List<String> mIdleTargetList;
	private String mTargetAfterAction;

	private int mComboPoints = 0;
	SimulationTarget mCPTarget;
	int mComboPointsGained;
	int mComboPointsGainedPrimalFury;
	int mComboPointsSpent;
	int mComboPointsOverflow;
	int mComboPointsLostTargetSwitch;

	EventMeleeSwing mMeleeSwingEv;
	EventEnergyTick mEnergyTickEv;
	EventSavageRoar mSavageRoarEv;
	EventOoC mOocEv;
	EventBerserk mBerserkEv;
	EventTigersFury mTigersFuryEv;
	EventStampede mStampedeEv;
	EventRunAway mRunAwayEv;
	EventToTT mTottEv;
	EventHeroism mHeroismEv;
	EventUnholyFrenzy mUnholyFrenzyEv;
	EventTier_11_4pc mTier_11_4pcEv;
	EventKirilProc mKirilProcEv;
	EventRebirth mRebirthEv;
	EventTranquility mTranquilityEv;
	EventPredatorsSwiftness mPredatorsSwiftnessEv;
	EventLogDamage mLogDamageEv;

	FeralChargeAction mFeralChargeAction;
	PotionAction mPotionAction;
	SwipeAction mSwipeAction;

	boolean mShouldFF;
	boolean mShouldMangle;
	boolean mHavePotion;
	boolean mPrePotion;
	boolean mIsInMeleeRange;
	boolean mAggressiveFeralCharge;
	boolean mBurningTendon;

	double mBaseCritChance;
	double mFeralFFCooldownExpireTime;
	double mSkullBashCooldownExpireTime;
	double mShatteringThrowCooldownExpireTime;
	double mFurySwipesCooldownExpireTime;
	double mMaimCooldownExpireTime;
	double mIdleTimeExpireTime;

	CatModelSimState(SimulationEngine su) {
		mSimEngine = su;
		mModel = new CatModel(su.getToon(), su.getConfig());
		mModel.setSimulationEngine(mSimEngine);

		// Setup target related information
		mActiveTarget = mSimEngine.getPrimaryTarget();
		mActiveTarget.setTargetData(new CatTargetData(mActiveTarget, true));
		mActiveTargetData = (CatTargetData) mActiveTarget.getTargetData();
		mTargetMap = new HashMap<String,SimulationTarget>();
		mTargetMap.put(mActiveTarget.getTarget().getName(), mActiveTarget);
		mTargetList = new LinkedList<String>();
		mTargetList.add(mActiveTarget.getTarget().getName());
		mIdleTargetList = new LinkedList<String>();
		mTargetSpawnOrderList = new LinkedList<String>();
		mTargetSpawnOrderList.add(mActiveTarget.getTarget().getName());
		mModel.setTarget(mActiveTarget.getTarget());

		mBaseCritChance = mModel.getMeleeCritChance();
		mIsInMeleeRange = true;
		mAggressiveFeralCharge = su.getConfig().getBool(SimulatorProperty.AGGRESSIVEFERALCHARGE);

		mIsLogging = su.isLogging();
		mLog = su.getLog();

		//
		// Apply other buffs/debuffs.
		//
		mTottEv = new EventToTT(this);
		mHeroismEv = new EventHeroism(this);
		mUnholyFrenzyEv = new EventUnholyFrenzy(this);
		if (!mModel.buffsDebuffs.isDebuff(Debuff.SHATTERINGTHROW)) mShatteringThrowCooldownExpireTime = Double.MAX_VALUE;

		mShouldFF = mModel.buffsDebuffs.isDebuff(Debuff.ARMOR);
		mShouldMangle = mModel.buffsDebuffs.isDebuff(Debuff.BLEEDDMG);
		if (!mModel.buffsDebuffs.isDebuff(Debuff.ARMOR)) mFeralFFCooldownExpireTime = Double.MAX_VALUE;

		//
		// Preallocate the remaining events, and initialize the rest of our state.
		//
		mMeleeSwingEv = new EventMeleeSwing(this);
		mEnergyTickEv = new EventEnergyTick(this, mModel.baseEnergyCap, mModel.baseEnergyCap);
		mSavageRoarEv = new EventSavageRoar(this);
		mBurningTendon = mSimEngine.getConfig().getBool(SimulatorProperty.BURNINGTENDON);
		if (mBurningTendon) mSavageRoarEv.schedule(40.0);
		mOocEv = new EventOoC(this);
		mStampedeEv = new EventStampede(this);
		if (mBurningTendon) mStampedeEv.run();
		mBerserkEv = new EventBerserk(this);
		mTigersFuryEv = new EventTigersFury(this);
		mRunAwayEv = new EventRunAway(this);
		mTier_11_4pcEv = new EventTier_11_4pc(this);
		mKirilProcEv = new EventKirilProc(this);
		mPredatorsSwiftnessEv = new EventPredatorsSwiftness(this);
		if (su.getConfig().getBool(SimulatorProperty.LOGDAMAGE)) mLogDamageEv = new EventLogDamage(this);

		// WARNING: Rebirth must be scheduled before Tranquility so that the conflict detection works.
		mRebirthEv = new EventRebirth(this);
		mTranquilityEv = new EventTranquility(this);

		mSimEngine.registerProcEffectCallback(Effect.PHYSICAL_DAMAGE, new EffectCallback() {
			@Override
			public void onEffect(SimulationTarget target, int n) {
				if (n <= 0) return;
				if (target == null) throw new RuntimeException("Physical damage proc triggered with no target.");

				Target curTarget = (mActiveTarget != null) ? mActiveTarget.getTarget() : null;
				mModel.setTarget(target.getTarget());

				AttackResult result = mModel.getYellowResult();
				double damage = n;

				if (result != AttackResult.MISS && result != AttackResult.DODGE) {
					if (result == AttackResult.CRIT) {
						damage *= mModel.multCrit;
					}

					damage *= getDirectDamageMult();

					((CatTargetData) target.getTargetData()).mProcPhysDamage += damage;

					if (mIsLogging) mLog.logAppend(" %s %s Damage: %d", target.getTarget().getName(), result, Math.round(damage));
				} else {
					if (mIsLogging) mLog.logAppend(" %s %s",target.getTarget().getName(), result);
				}

				mModel.setTarget(curTarget);
			}
		});
		mSimEngine.registerProcEffectCallback(Effect.SPELL_DAMAGE, new EffectCallback() {
			@Override
			public void onEffect(SimulationTarget target, int n) {
				if (n <= 0) return;
				if (target == null) throw new RuntimeException("Physical damage proc triggered with no target.");

				Target curTarget = (mActiveTarget != null) ? mActiveTarget.getTarget() : null;
				mModel.setTarget(target.getTarget());

				AttackResult result = mModel.getSpellResult();
				double damage = n;

				if (result != AttackResult.MISS) {
					if (result == AttackResult.CRIT) {
						damage *= mModel.multSpellCrit;
					}

					damage *= getSpellDamageMult(target);
					((CatTargetData) target.getTargetData()).mProcSpellDamage += damage;

					if (mIsLogging) mLog.logAppend(" %s %s Damage: %d", target.getTarget().getName(), result, Math.round(damage));
				} else {
					if (mIsLogging) mLog.logAppend(" %s MISS", target.getTarget().getName());
				}

				mModel.setTarget(curTarget);
			}
		});
		mSimEngine.registerProcEffectCallback(Effect.SPELL_ID, new EffectCallback() {
			int mMatrixRestabilizerNormalStat;
			int mMatrixRestabilizerHeroicStat;

			@Override
			public void onEffect(SimulationTarget target, int n) {
				// Matrix Restabilizer
				if (n == 69150 || n == 68994) {
					double critRating = mModel.getCritRating();
					double hasteRating = mModel.getHasteRating();
					double masteryRating = mModel.getMasteryRating();

					/*
					 * Cases tested by my minion:
					 * 0 of all ratings - Mastery
					 * 77 Mastery, 177 Crit and 103 Haste - Crit (No interaction with base mastery)
					 * 143 Mastery, 143 Crit and 0 Haste - Mastery
					 * 115 Mastery, 143 Crit, and 143 Haste - Haste
					 *
					 * It seems reasonable to conclude that the trinket follows Mastery > Haste > Crit
					 */
					int stateVar = 0;
					if (masteryRating >= critRating && masteryRating >= hasteRating) {
						stateVar = 2;
						mModel.addMasteryRating(Procs.getInstance().getProcByItemID(n).get(Effect.MASTERY_RATING));
						if (mIsLogging) mLog.logAppend(" Proc: Mastery Rating");
					} else if (hasteRating >= critRating && hasteRating >= masteryRating) {
						stateVar = 1;
						mModel.addHasteRating(Procs.getInstance().getProcByItemID(n).get(Effect.HASTE_RATING));
						if (mIsLogging) mLog.logAppend(" Proc: Haste Rating");
					} else if (critRating >= hasteRating && critRating >= masteryRating) {
						stateVar = 0;
						mModel.addCritRating(Procs.getInstance().getProcByItemID(n).get(Effect.CRIT_RATING));
						if (mIsLogging) mLog.logAppend(" Proc: Crit. Rating");
					}

					if (n == 69150) mMatrixRestabilizerHeroicStat = stateVar; else mMatrixRestabilizerNormalStat = stateVar;
				} else if (n == -69150 || n == -68994) {
					int stateVar = (n == -69150) ? mMatrixRestabilizerHeroicStat: mMatrixRestabilizerNormalStat;
					switch (stateVar) {
					case 0: mModel.addCritRating(-Procs.getInstance().getProcByItemID(-n).get(Effect.CRIT_RATING)); break;
					case 1: mModel.addHasteRating(-Procs.getInstance().getProcByItemID(-n).get(Effect.HASTE_RATING)); break;
					case 2: mModel.addMasteryRating(-Procs.getInstance().getProcByItemID(-n).get(Effect.MASTERY_RATING)); break;
					}
				}

				// DMC:Hurricane
				// As of 4.0.6, use the melee crit multipler/hit table.
				if (n == 62051) {
					if (target == null) throw new RuntimeException("DMC:H proc triggered with no target.");

					Target curTarget = (mActiveTarget != null) ? mActiveTarget.getTarget() : null;
					mModel.setTarget(target.getTarget());

					// Just use the stealth yellow attack table resolution since it ignores Dodge/Glance.
					AttackResult result = mModel.getYellowStealthResult(0);
					double damage = Procs.getInstance().getProcByItemID(n).get(Effect.SPELL_DAMAGE);

					if (result != AttackResult.MISS) {
						// As of 4.2 PTR, this is no longer capable of criting.
//						if (result == AttackResult.CRIT) {
//							damage *= mModel.multCrit;
//						}

						damage *= getSpellDamageMult(target);
						((CatTargetData) target.getTargetData()).mProcSpellDamage += damage;

						if (mIsLogging) mLog.logAppend(" %s Damage: %d", target.getTarget().getName(), Math.round(damage));
					} else {
						if (mIsLogging) mLog.logAppend(" %s MISS", target.getTarget().getName());
					}

					mModel.setTarget(curTarget);
				}

				// Vial of Shadows
				if (n == 77999 || n == 77207 || n == 77979) {
					if (target == null) throw new RuntimeException("Vial of Shadows proc triggered with no target.");

					Target curTarget = (mActiveTarget != null) ? mActiveTarget.getTarget() : null;
					mModel.setTarget(target.getTarget());

					AttackResult result = (mActiveTargetData.mCanParry) ? mModel.getFrontalYellowResult() : mModel.getYellowResult();

					if (result == AttackResult.MISS) {
						if (mIsLogging) mLog.logAppend(" %s MISS", target.getTarget().getName());
					} else if (result == AttackResult.DODGE) {
						if (mIsLogging) mLog.logAppend(" %s DODGE", target.getTarget().getName());
					} else if (result == AttackResult.PARRY) {
						if (mIsLogging) mLog.logAppend(" %s PARRY", target.getTarget().getName());
					} else {
						double damage = Procs.getInstance().getProcByItemID(n).get(Effect.PHYSICAL_DAMAGE);
						double coeff = 0.0;
						switch (n) {
						case 77999: coeff = 0.333; break;	// Heroic
						case 77207: coeff = 0.300; break;	// Normal
						case 77979: coeff = 0.266; break;	// LFR
						}

						damage += mModel.getAttackPower() * coeff;

						if (result == AttackResult.CRIT) {
							damage *= mModel.multCrit;
						}

						// XXX: Is this mitigated by armor? (Assumes yes).
						damage *= getDirectDamageMult();
						((CatTargetData) target.getTargetData()).mProcPhysDamage += damage;

						if (mIsLogging) mLog.logAppend(" %s Damage: %d", target.getTarget().getName(), Math.round(damage));
					}

					mModel.setTarget(curTarget);
				}

				// Kiril, Fury of the Beasts
				// Note: The event code handles cleanup on it's own since it schedules an event to do ramp up.
				if (n == 78473 || n == 77194 || n == 78482) {
					mKirilProcEv.run(n);
				}

				// Troll Berserking (Racial)
				if (n == 26297) {
					mModel.multiplyMeleeHaste(1.2);
					mModel.multiplySpellHaste(1.2);
				} else if (n == -26297) {
					mModel.multiplySpellHaste(1.0 / 1.2);
					mModel.multiplyMeleeHaste(1.0 / 1.2);
				}
			}
		});

		for (ProcEffect effect : Procs.getInstance().getProcsByProperty(mSimEngine.getConfig().getProperties())) {
			if (effect.triggers.equals(EnumSet.of(ProcEffect.Trigger.POTION))) {
				mHavePotion = true;
				mPrePotion = su.getConfig().getBool(Property.SIMULATION_PREPOTION);
			} else if (!effect.triggers.contains(ProcEffect.Trigger.USER) && effect.rate == 0 && effect.ppm == 0) {
				mSimEngine.getView().onWarn("Proc effect \"" + effect.name + "\" has no rate information.");
				continue;
			}

			mSimEngine.addProc(new SimulationProc(mSimEngine, mModel, effect));
		}

		mFeralChargeAction = new FeralChargeAction(this);
		mPotionAction = new PotionAction(this);
		mSwipeAction = new SwipeAction(this);

		mSimEngine.addAction(Action.FERALCHARGE, mFeralChargeAction, false);
		mSimEngine.addAction(Action.TIGERSFURY, mTigersFuryEv, false);
		mSimEngine.addAction(Action.POTION, mPotionAction, false);
		mSimEngine.addAction(Action.HEROISM, mHeroismEv, false);
		mSimEngine.addAction(Action.UNHOLYFRENZY, mUnholyFrenzyEv, false);
		mSimEngine.addAction(Action.SKULLBASH, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				if (mSkullBashCooldownExpireTime > mSimEngine.getSimTime())
					return ACTION_FAIL;

				return mActiveTargetData.mSkullBashAction.perform();
			}
		}, false);
		mSimEngine.addAction(Action.SHATTERINGTHROW, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				if (mShatteringThrowCooldownExpireTime > mSimEngine.getSimTime())
					return ACTION_FAIL;

				mShatteringThrowCooldownExpireTime = mSimEngine.getSimTime() + mModel.shatteringThrowCDDuration;

				return mActiveTargetData.mShatteringThrowEv.perform();
			}
		}, false);
		mSimEngine.addAction(Action.RAVAGE, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				return mActiveTargetData.mRavageAction.perform();
			}
		});
		mSimEngine.addAction(Action.POUNCE, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				return mActiveTargetData.mPounceAction.perform();
			}
		});
		mSimEngine.addAction(Action.CLAW, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				return mActiveTargetData.mClawAction.perform();
			}
		});
		mSimEngine.addAction(Action.MANGLE, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				return mActiveTargetData.mMangleAction.perform();
			}
		});
		mSimEngine.addAction(Action.RAKE, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				return mActiveTargetData.mRakeAction.perform();
			}
		});
		mSimEngine.addAction(Action.SHRED, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				return mActiveTargetData.mShredAction.perform();
			}
		});
		mSimEngine.addAction(Action.RIP, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				return mActiveTargetData.mRipAction.perform();
			}
		});
		mSimEngine.addAction(Action.BITE, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				return mActiveTargetData.mFerociousBiteAction.perform();
			}
		});
		mSimEngine.addAction(Action.ROAR, mSavageRoarEv);
		mSimEngine.addAction(Action.MAIM, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				if (mMaimCooldownExpireTime > mSimEngine.getSimTime())
					return ACTION_FAIL;

				double rval = mActiveTargetData.mMaimAction.perform();

				if (rval != ACTION_FAIL) mMaimCooldownExpireTime = mSimEngine.getSimTime() + mModel.maimCDDuration;

				return rval;
			}
		});
		mSimEngine.addAction(Action.SWIPE, mSwipeAction);
		mSimEngine.addAction(Action.FERALFF, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				if (mFeralFFCooldownExpireTime > mSimEngine.getSimTime())
					return ACTION_FAIL;

				mFeralFFCooldownExpireTime = mSimEngine.getSimTime() + mModel.feralFFCDDuration;

				return mActiveTargetData.mFeralFFAction.perform();
			}
		});
		mSimEngine.addAction(Action.BERSERK, mBerserkEv, false);
		mSimEngine.addAction(Action.RUNAWAY, mRunAwayEv, false);

		mSimEngine.addAction(Action.AUTOATTACK_STOP, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTarget == null) return ACTION_FAIL;

				mMeleeSwingEv.stop();
				return 0.0;
			}
		}, false);
		mSimEngine.addAction(Action.AUTOATTACK_START, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTarget == null) return ACTION_FAIL;

				mMeleeSwingEv.start();
				return 0.0;
			}
		}, false);

		mSimEngine.addAction(Action.NONE, new SimulationAction() {
			@Override
			public double perform() {
				return ACTION_FAIL;
			}
		});
	}

	//
	// Target management.
	//

	private final void swapToTarget(SimulationTarget nextTarget, boolean wipeDebuffs) {
		SimulationTarget prevTarget = mActiveTarget;

		if (nextTarget == prevTarget) return;

		CatTargetData nextTargetData = (nextTarget != null) ? (CatTargetData) nextTarget.getTargetData() : null;
		if (nextTargetData != null) {
			if (!nextTargetData.mDeathEv.isUp()) return;		// No swapping to a dead target.
		} else
			mMeleeSwingEv.stop();	// Detargeting, stop the swing timer.

		if (mActiveTarget != null) {
			if (wipeDebuffs) wipeTargetDebuffs(mActiveTarget);
		}

		// Note: Swapping targets does not reset the swing timer.

		mActiveTarget = nextTarget;
		mActiveTargetData = nextTargetData;
		mModel.setTarget((mActiveTarget != null) ? mActiveTarget.getTarget() : null);
	}

	private final void wipeTargetDebuffs(SimulationTarget target) {
		CatTargetData targetData = (CatTargetData) target.getTargetData();

		if (!mSimEngine.getConfig().getBool(SimulatorProperty.ASSUMEEXTERNALFF)) {
			targetData.mFeralFFAction.cancel();
		}

		if (!mSimEngine.getConfig().getBool(SimulatorProperty.ASSUMEEXTERNALMANGLE)) {
			targetData.mMangleAction.cancel();
		}

		targetData.mRakeAction.cancel();
		targetData.mRipAction.cancel();
		targetData.mTier_12_2pcEv.cancel();
		targetData.mShatteringThrowEv.cancel();
	}

	//
	// Damage sources/various triggers.
	//

	final double getDirectDamageMult() {
		return getDirectDamageMult(mActiveTarget);
	}

	final double getDirectDamageMult(SimulationTarget target) {
		double mult = 1;

		mult *= (1 - target.getTargetData().getArmorDR(mModel.mToon.level));
		mult *= (mTigersFuryEv.isUp()) ? mModel.multTigersFury : 1.0;
		mult *= mModel.multPhysVuln;
		mult *= mModel.multDamageMult;
		mult *= mTottEv.getMult();
		mult *= target.getTarget().getResilienceDRMult();

		return mult;
	}

	final double getBleedDamageMult(double masteryMult, CatTargetData targetData) {
		double mult = 1;

		// ToTT is handled elsewhere.
		mult *= mModel.multBleedDarkIntent;
		mult *= masteryMult;
		mult *= targetData.mMangleAction.isUp() ? mModel.multMangle : 1;
		mult *= mModel.multPhysVuln * mModel.multDamageMult;
		mult *= targetData.getSimulationTarget().getTarget().getResilienceDRMult();

		return mult;
	}

	final double getSpellDamageMult(SimulationTarget target) {
		double mult = 1;

		mult *= mModel.multSpellVuln;
		mult *= mModel.multDamageMult;
		mult *= mTottEv.getMult();
		mult *= target.getTarget().getResilienceDRMult();

		return mult;
	}

	long mEnergySpent;

	final int onOoCConsumer(int cost) {
		if (mOocEv.isUp()) {
			mOocEv.cancel();
			mOocEv.mNrOoCProcsConsumed++;
			mOocEv.mOocEnergyReturn += cost;

			if (mIsLogging) mLog.log("Omen of Clarity Consumed\n");

			return 0;
		} else {
			mEnergyTickEv.mEnergy -= cost;
			mEnergySpent += cost;
		}

		return cost;
	}

	final int onEnergyConsumer(int cost) {
		// Stampede - Ravage, Savage Roar, Feral Charge and Skull Bash do not consume OoC.
		mEnergyTickEv.mEnergy -= cost;
		mEnergySpent += cost;

		return cost;
	}

	//
	// Ability use
	//
	final void onCpGenerator(boolean crit) {
		if (mActiveTarget != mCPTarget) {
			mComboPointsLostTargetSwitch = mComboPoints;
			mComboPoints = 0;
			mCPTarget = mActiveTarget;
		}

		if (crit) {
			if (mModel.pPrimalFury == 1.0 || mSimEngine.getRandom().nextBoolean(mModel.pPrimalFury)) {
				mComboPoints += 2;
				mComboPointsGainedPrimalFury++;
				mComboPointsGained += 2;
			} else {
				mComboPoints++;
				mComboPointsGained++;
			}
		} else {
			mComboPoints++;
			mComboPointsGained++;
		}

		if (mComboPoints > 5) {
			mComboPointsOverflow += mComboPoints - 5;
			mComboPoints = 5;
		}
	}

	final void onFinisher() {
		mPredatorsSwiftnessEv.onFinisher(mComboPoints);
		mComboPointsSpent += mComboPoints;
		mComboPoints = 0;
	}

	final void logEnergyChange(int prevEnergy) {
		if (mIsLogging) logResourceChange(prevEnergy, mComboPoints);
	}

	final void logComboPointChange(int prevComboPoints) {
		if (mIsLogging) logResourceChange(getEnergy(), prevComboPoints);
	}

	final void logResourceChange(int prevEnergy, int prevCPs) {
		if (mIsLogging) {
			if (prevEnergy != getEnergy() && prevCPs != mComboPoints) {
				mLog.log("Energy: %d -> %d / %d CPs: %d -> %d\n", prevEnergy, getEnergy(), getEnergyCap(), prevCPs, mComboPoints);
			} else if (prevEnergy != getEnergy()) {
				mLog.log("Energy: %d -> %d / %d CPs: %d\n", prevEnergy, getEnergy(), getEnergyCap(), mComboPoints);
			} else if (prevCPs != mComboPoints) {
				mLog.log("Energy: %d / %d CPs: %d -> %d\n", getEnergy(), getEnergyCap(), prevCPs, mComboPoints);
			} else {
				mLog.log("Energy: %d / %d CPs: %d\n", getEnergy(), getEnergyCap(), mComboPoints);
			}
		}
	}

	final int getEnergyCost(int baseCost) {
		return mBerserkEv.isUp() ? baseCost / 2 : baseCost;
	}

	int mPrimalMadnessTriggers;
	long mPrimalMadnessEnergyGained;
	long mPrimalMadnessEnergyLost;

	final void onPrimalMadnessGain() {
		if (mModel.primalMadnessEnergyGain > 0) {
			mPrimalMadnessTriggers++;
			if (mEnergyTickEv.mEnergyCap == mModel.baseEnergyCap) {
				mEnergyTickEv.mEnergyCap += mModel.primalMadnessEnergyGain;

				mPrimalMadnessEnergyGained += mModel.primalMadnessEnergyGain;
				mEnergyTickEv.onEnergyGain(mModel.primalMadnessEnergyGain);

				if (mIsLogging) {
					mLog.log("Primal Madness: Energy: %d / %d\n", getEnergy(), getEnergyCap());
				}
			}
		}
	}

	final void onPrimalMadnessLoss(boolean force) {
		if (mModel.primalMadnessEnergyGain > 0) {
			if (mEnergyTickEv.mEnergyCap > mModel.baseEnergyCap) {
				if (!force && (mBerserkEv.isUp() || mTigersFuryEv.isUp())) return;

				mEnergyTickEv.mEnergyCap = mModel.baseEnergyCap;

				if (mEnergyTickEv.mEnergy > mModel.baseEnergyCap) {
					mEnergyTickEv.mEnergyOverflow += mEnergyTickEv.mEnergy - mModel.baseEnergyCap;
					mEnergyTickEv.mEnergy = mModel.baseEnergyCap;
				}
				if (mEnergyTickEv.mEnergy > mModel.primalMadnessEnergyGain) {
					mPrimalMadnessEnergyLost += mModel.primalMadnessEnergyGain;
				} else {
					mPrimalMadnessEnergyLost += mEnergyTickEv.mEnergy;
				}

				mEnergyTickEv.mEnergy = Math.max(0, mEnergyTickEv.mEnergy - mModel.primalMadnessEnergyGain);

				if (mIsLogging) {
					mLog.log("Primal Madness Expiration: Energy: %d / %d\n", getEnergy(), getEnergyCap());
				}
			}
		}
	}
}
